Configurar y utilizar los puertos de entrada analógica del microcontrolador.
En el caso del Atmega168pa tenemos 6 puertos que nos permiten hacer conversión analógico-digital:
Para utilizar el ADC (Analog Digital Converter) tenemos que realizar lo siguiente:
ADMUX |= (1 << REFS0);
NOTA: el datasheet del atmega168pa nos recomienda conectar AREF a tierra con un condensador (se recomienda 10nf o 100nf) para estabilizar el voltaje.
Para obtener una alta resolución del ADC tendremos que configurar el prescaler de manera que la frecuencia resultante (Frecuencia de reloj / prescaler) sea de entre 50Khz y 200Khz, (con un reloj de 8Mhz y un prescaler de 64 serían 125Khz por ejemplo), si queremos podemos establecer un valor por encima de 200Khz pero a cambio de perder resolución en el valor final que obtenemos del ADC.
El conversor ADC tarda unos 13 "ciclos ADC" en obtener una lectura, si tenemos un reloj de 8Mhz con un prescaler de 64 (ADC a 125Khz) el ADC tardaría unos 100 microsegundos en leer el valor.
Para establecer un prescaler de 64 sería:
ADCSRA |= (1 << ADPS1) | (1 << ADPS2);
ADCSRA |= (1 << ADEN);
ADCSRA |= (1 << ADSC);
loop_until_bit_is_clear(ADCSRA, ADSC);
El convertidor analógico-digital a pesar de tener varios puertos disponibles solo puede usar uno a la vez (si queremos obtener la entrada analógica en varios puertos tenemos que hacer multiplexación).
Para cambiar el puerto sobre el que se obtienen las lecturas se hace a traves de los 4 bits menos significativos del registro ADMUX, en este caso no es una mascara, sino que es el numero del puerto en sí (ya que solo puede ser uno el puerto activo para el ADC).
Para indicar el puerto podemos hacer un OR con el número del puerto (o macro del puerto) pero tenemos que dejar los 4 bits más significativos sin cambiar.
Para activar el puerto PC3 del microcontrolador como puerto para el ADC lo haríamos tal que así:
ADMUX = (0xf0 & ADMUX) | PC3;
Esta operación nos permite mantener los 4 bits más significativos sin alterar y dejar a 0 los 4 menos significativos, para poder hacer un OR del puerto que queramos y que no intefiera con alguna valor previo.
Al hacer un OR con el puerto 3 "00000011" solo se cambian los bits menos significativos con el valor binario "3"
Al añadir la resistencia para realizar el divisor de voltaje en el circuito tenemos que tener en cuenta cual es el rango de valores en los que se mueve nuestro sensor.
Si por ejemplo tenemos un sensor que en su rango minimo nos da una resistencia de 10K Ohm y la resistencia que usamos para el divisor de voltaje es de 10K Ohm en este caso la lectura más baja que nos dará el ADC será de 512 (ya que tenemos 10K Ohm en las dos partes del divisor el votaje será la mitad), en este caso tendríamos que considerar usar una resistencia mas grande para el divisor de voltaje para poder obtener un rango de lectura mayor.
Por defecto el conversor ADC trabaja en modo Single-shot, nosotros iniciamos el momento en el que hacer una lectura, otro de los modos de funcionamiento es el free-run, en este modo el conversor ADC está constantemente haciendo lecturas, nosotros solo tenemos que leer el valor ADC para obtener la ultima actualización.
Para usar el modo free-run solo tenemos que activar el auto-trigger del ADC (el modo free-run es el modo por defecto del auto-trigger):
ADCSRA |= (1 << ADATE);
A pesar de que no tenemos que indicar cuando queremos hacer cada lectura si que tenemos que indicar una primera lectura para que el modo free-run empiece a hacer lecturas constantes:
ADCSRA |= (1 << ADSC);
Podemos hacer que se genere una interrupción cada vez que se complete una lectura del ADC, activamos la interrupciones del ADC:
ADCSRA |= (1 << ADIE);
Y activamos las interrupciones globales:
sei();
Definimos la ISR() que se ejecutará al lanzarse la interrupción:
ISR(ADC_vect) {}
A continuación se muestra un código de ejemplo que usa el modo free-run y las interrupciones al completarse cada lectura del ADC:
#include <avr/io.h>
#include <avr/power.h>
#include <avr/interrupt.h>
volatile uint8_t ledValue;
volatile uint16_t adcValue;
void ADC_init() {
ADMUX |= (1 << REFS0);
ADCSRA |= (1 << ADPS0) | (1 << ADPS1);
ADCSRA |= (1 << ADEN);
ADCSRA |= (1 << ADIE); // Activamos interrupciones para el ADC
ADCSRA |= (1 << ADATE); // Activamos auto trigger del ADC (Por defecto el modo es el free-run)
ADCSRA |= (1 << ADSC); // Iniciamos la primera conversión (a partir de aqui el free-run se ejecutara de manera automatica)
sei();
}
ISR(ADC_vect) {
adcValue = ADC; // Leemos el valor del ADC
ledValue = (adcValue >> 7);
PORTB = 0;
for (uint8_t i = 0; i <= ledValue; i++) {
PORTB |= (1 << i);
}
// Clear the interrupt flag to allow next conversion
ADCSRA |= (1 << ADIF);
}
int main(void) {
clock_prescale_set(clock_div_1);
DDRB = 0xFF; //Todos los puertos del registro B como salida
ADC_init();
while (1) {
}
}
AVR | microcontrolador | Analog